﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using ManoMachine.Assembler.Exceptions;
using ManoMachine.Assembler.Mnemonics.Descriptors;
using ManoMachine.Assembler.Parsers;
using ManoMachine.Common;
using ZiadSpace.Util;

namespace ManoMachine.Assembler
{
    public class Assembler
    {
        private SymbolsTable _symbolsTable;

        public SymbolsTable SymbolsTable
        {
            get { return this._symbolsTable; }
            private set { this._symbolsTable = value; }
        }

        private MemoryMap _memoryMap;

        public MemoryMap MemoryMap
        {
            get { return this._memoryMap; }
            private set { this._memoryMap = value; }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Assembler"/> class.
        /// </summary>
        public Assembler()
        {
            this.Restart();
        }

        public void Restart()
        {
            this.SymbolsTable = new SymbolsTable();
            this.MemoryMap = new MemoryMap();
        }

        /// <summary>
        /// Assemble an assembly code.
        /// </summary>
        /// <remarks>
        /// The <paramref name="assemblyCode"/> parameter will be divided by the "Environment.NewLine" string.
        /// </remarks>
        /// <param name="assemblyCode">The assembly code in multiline form.</param>
        /// <param name="parallel">Allow parallel code scanning.</param>
        public AssemblerResult Assemble(string assemblyCode, bool parallel)
        {
            return this.Assemble(assemblyCode.Split(new string[] { "\n" }, StringSplitOptions.None), parallel);
        }

        /// <summary>
        /// Assemble an assembly code.
        /// </summary>
        /// <param name="assemblyCode">The assembly code in multiline form.</param>
        /// <param name="parallel">Allow parallel code scanning.</param>
        public AssemblerResult Assemble(string[] assemblyCode, bool parallel)
        {
            AssemblerResult assemblerResult = new AssemblerResult
                                                  {
                                                      Errors = new ObservableCollection<Tuple<int, string>>(),
                                                      SymbolsTable = new ObservableCollection<Tuple<string, ushort>>(),
                                                      MemoryMap = new ObservableCollection<Tuple<ushort, short>>(),
                                                  };
            try
            {
                ///////////////////////////////////////////////////////////////////////////////////////////
                // Prepare
                ///////////////////////////////////////////////////////////////////////////////////////////
                int lineNumber = 0;
                List<AssemblyLine> lines = new List<AssemblyLine>(assemblyCode.Length);
                foreach (string lineCode in assemblyCode)
                {
                    lineNumber++;
                    if (!string.IsNullOrWhiteSpace(lineCode))
                    {
                        AssemblyLine assemblyLine = new AssemblyLine { Code = lineCode, Number = lineNumber };
                        lines.Add(assemblyLine);
                    }
                }

                ///////////////////////////////////////////////////////////////////////////////////////////
                // Parse and validate
                ///////////////////////////////////////////////////////////////////////////////////////////
                Stopwatch stopwatch = Stopwatch.StartNew();
                if (parallel)
                {
                    ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = -1 };
                    Parallel.ForEach(lines, parallelOptions, ParseAndValidate);
                }
                else
                {
                    foreach (var line in lines) ParseAndValidate(line);
                }
                long elapsedTicks = stopwatch.ElapsedTicks;
                ConsoleColor foregroundColor = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("ElapsedTicks: [{0}]", elapsedTicks);
                Console.ForegroundColor = foregroundColor;

                ///////////////////////////////////////////////////////////////////////////////////////////
                // Collect the AssemblerExceptions
                ///////////////////////////////////////////////////////////////////////////////////////////
                List<AssemblerException> assemblerExceptions = new List<AssemblerException>();
                foreach (AssemblyLine line in lines)
                {
                    if (line.Error != null)
                    {
                        assemblerExceptions.Add(line.Error);
                    }
                }
                if (assemblerExceptions.Count > 0) throw new AggregateAssemblerException(assemblerExceptions);

                ///////////////////////////////////////////////////////////////////////////////////////////
                // Pass 1 Assembler
                ///////////////////////////////////////////////////////////////////////////////////////////
                DoPass1(lines);
                ///////////////////////////////////////////////////////////////////////////////////////////
                // Pass 2 Assembler
                ///////////////////////////////////////////////////////////////////////////////////////////
                DoPass2(lines);

                ///////////////////////////////////////////////////////////////////////////////////////////
                // Setup AssemblerResult
                ///////////////////////////////////////////////////////////////////////////////////////////
                Dictionary<string, ushort>.Enumerator enumerator1 = this.SymbolsTable.GetEnumerator();
                while (enumerator1.MoveNext())
                {
                    KeyValuePair<string, ushort> obj = enumerator1.Current;
                    assemblerResult.SymbolsTable.Add(new Tuple<string, ushort>(obj.Key, obj.Value));
                }
                Dictionary<ushort, short>.Enumerator enumerator2 = this.MemoryMap.GetEnumerator();
                while (enumerator2.MoveNext())
                {
                    KeyValuePair<ushort, short> obj = enumerator2.Current;
                    assemblerResult.MemoryMap.Add(new Tuple<ushort, short>(obj.Key, obj.Value));
                }
                if (assemblerResult.MemoryMap.Count >= 1)
                {
                    assemblerResult.StartAddress = assemblerResult.MemoryMap[0].Item1;
                }
                ///////////////////////////////////////////////////////////////////////////////////////////
            }
            catch (AggregateAssemblerException aggregateAssemblerException)
            {
                foreach (AssemblerException exception in aggregateAssemblerException.InnerExceptions)
                {
                    assemblerResult.Errors.Add(new Tuple<int, string>(exception.Line.Number, exception.Message));
                }
            }
            catch (AssemblerException exception)
            {
                assemblerResult.Errors.Add(new Tuple<int, string>(exception.Line.Number, exception.Message));
            }
            catch (Exception exception)
            {
                assemblerResult.Errors.Add(new Tuple<int, string>(0, "Assembler Exception: " + exception.Message));
            }
            return assemblerResult;
        }

        /// <summary>
        /// Parse and validate an assembly line code
        /// </summary>
        /// <param name="assemblyLine"></param>
        private static void ParseAndValidate(AssemblyLine assemblyLine)
        {
            try
            {
                AssemblyLineParser.Parse(ref assemblyLine);
            }
            catch (AssemblerException)
            {
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="lines">The input MUST be correct</param>
        private void DoPass1(List<AssemblyLine> lines)
        {
            int lc = 0;
            foreach (AssemblyLine line in lines)
            {
                // ignore the empty lines
                if (line.MnemonicDescriptor == null)
                    continue;

                string instruction = line.InstructionParts[0];
                if (string.IsNullOrWhiteSpace(line.Label) == true)
                {
                    if (string.Compare(instruction, "Org", true) == 0)
                    {
                        PSIDescriptor descriptor = line.MnemonicDescriptor as PSIDescriptor;
                        int number = descriptor.GetNumber(line);
                        lc = number;
                    }
                    else if (string.Compare(instruction, "End", true) == 0)
                    {
                        return;
                    }
                    else
                    {
                        lc++;
                        continue;
                    }
                }
                else
                {
                    SymbolsTable.Insert(line.Label, (ushort)lc);
                    lc++;
                }
            }
        }

        private void DoPass2(List<AssemblyLine> lines)
        {
            int lc = 0;
            foreach (AssemblyLine line in lines)
            {
                // ignore the empty lines
                if (line.MnemonicDescriptor == null)
                    continue;

                // <> PseudoInstruction
                if (line.MnemonicDescriptor is PSIDescriptor)
                {
                    // <> ORG
                    if (string.Compare("Org", line.InstructionParts[0], true) == 0)
                    {
                        PSIDescriptor descriptor = line.MnemonicDescriptor as PSIDescriptor;
                        int number = descriptor.GetNumber(line);
                        lc = number;
                    }
                    else
                    {
                        // <> END
                        if (string.Compare(line.InstructionParts[0], "End", true) == 0)
                        {
                            return;
                        }
                        else
                        {
                            // [] Convert operand to binary and story in location given by LC
                            PSIDescriptor descriptor = line.MnemonicDescriptor as PSIDescriptor;
                            int number = descriptor.GetNumber(line);
                            this.MemoryMap[lc] = (short)number;
                            // [] Increment LC
                            lc++;
                        }
                    }
                }
                else
                {
                    // <> MRI
                    if (line.MnemonicDescriptor is MRIDescriptor)
                    {
                        // [] Get operation code and set bits 2-4
                        MRIDescriptor mriDescriptor = line.MnemonicDescriptor as MRIDescriptor;
                        ushort operationCode = mriDescriptor.OperationCode;
                        // [] Search address-symbol table for binary equivalent of symbolic address and set bits 5-16
                        string symbolicAddress = mriDescriptor.GetSymbolicAddress(line);
                        if (!SymbolsTable.Contains(symbolicAddress))
                        {
                            throw new AssemblerException("PASS 2: Invalid symbolic address", line);
                        }
                        ushort address = SymbolsTable.GetAddress(symbolicAddress);
                        // [] Indirect Memory
                        ushort indirectMemory = (ushort)(mriDescriptor.GetIndirectMemory(line) ? 1 : 0);
                        // [] Assemble all parts of binary instruction and story in location given by LC
                        indirectMemory = (ushort)BitHelper.SetBits(0, indirectMemory, 1, 16, 1);
                        operationCode = (ushort)BitHelper.SetBits(0, operationCode, 1, 13, 3);
                        address = (ushort)BitHelper.SetBits(0, address, 1, 1, 12);

                        int i = (indirectMemory | operationCode | address);
                        MemoryMap[lc] = (short?)i;

                        lc++;
                    }
                    else
                    {
                        // Condition is always True
                        // if (line.MnemonicDescriptor is RRIDescriptor || line.MnemonicDescriptor is MRIDescriptor)

                        InstructionDescriptor instructionDescriptor = line.MnemonicDescriptor as InstructionDescriptor;
                        MemoryMap[lc] = (short?)instructionDescriptor.OperationCode;
                        lc++;
                    }
                }
                // MUST Remain Empty
            }
        }
    }
}